﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Web;
using System.IO;
using System.Web.Caching;

namespace WebsitePanel.IIsModules
{
    public partial class SecureFolders : IHttpModule
    {
        // constants
        const string HTACCESS_FILENAME = ".htaccess";
        const string HTPASSWD_FILENAME = ".htpasswd";
        const string HTGROUP_FILENAME = ".htgroup";

        // fields
        HtAccess config;

        #region IHttpModule Handlers
        public void Init(HttpApplication context)
        {
            context.AuthenticateRequest += new EventHandler(context_OnEnter);
            context.EndRequest += new EventHandler(content_OnLeave);
        }

        void context_OnEnter(object sender, EventArgs e)
        {
            HttpContext context = HttpContext.Current;

            // system files could not be downloaded
            string requestedFile = Path.GetFileName(context.Request.PhysicalPath);
            if (requestedFile == HTACCESS_FILENAME
                || requestedFile == HTPASSWD_FILENAME
                || requestedFile == HTGROUP_FILENAME)
            {
                NotFound();
                return;
            }

            // check for .htaccess
            try
            {
                if (!LoadHtAccess())
                    return; // no .htaccess found up to web site root
            }
            catch (HtAccessException ex)
            {
                WriteError(ex.Message, ex.FilePath, ex.LineText);
            }

            string authHeader = context.Request.Headers["Authorization"];
            if (String.IsNullOrEmpty(authHeader))
            {
                // Send Basic authentication request
                DenyAccess();
                return;
            }

            //if (authHeader.StartsWith("Basic"))
            //{
            //    // Don't understand this header...we'll pass it along and 
            //    // assume someone else will handle it
            //    DenyAccess();
            //    return;
            //}

            if (authHeader.StartsWith("Basic"))
            {
                // read credentials
                string encodedUserPass = authHeader.Substring(6).Trim();
                Encoding encoding = Encoding.GetEncoding("iso-8859-1");
                string userPass = encoding.GetString(Convert.FromBase64String(encodedUserPass));
                int separator = userPass.IndexOf(':');
                string username = userPass.Substring(0, separator);
                string password = userPass.Substring(separator + 1);

                // validate user credentials
                if (config.Users.ContainsKey(username))
                {
                    // check password
                    BsdDES des = new BsdDES();
                    string storedPassword = config.Users[username];
                    string cryptPassword = des.Crypt(password, storedPassword);
                    if (String.Compare(storedPassword, cryptPassword, true) == 0)
                    {
                        // user is valid
                        // perform authorization

                        // check for any valid user
                        if(config.RequiredUsers.Contains("valid-user"))
                            return; // success

                        // check for specific user
                        else if(config.RequiredUsers.Contains(username))
                            return; // user found - success
                    }
                }

                // Invalid credentials or access denied
                DenyAccess();
            }
        }

        void content_OnLeave(object sender, EventArgs e)
        {
            if (config != null && config.AuthType != AuthType.None
                && HttpContext.Current.Response.StatusCode == 401)
            {
                SendAuthenticationHeader();
            }
        }

        public void Dispose()
        {
        }
        #endregion

        #region Basic Authentication methods

        // sends 404 status
        private void NotFound()
        {
            HttpContext context = HttpContext.Current;
            context.Response.StatusCode = 404;
            context.Response.End();
        }

        // set a 401 and end the current request
        // processing will jump directly to EndRequest
        private void DenyAccess()
        {
            HttpContext context = HttpContext.Current;
            context.Response.StatusCode = 401;
            context.Response.End();
        }

        // send header to start Basic Authentication handshake
        private void SendAuthenticationHeader()
        {
            HttpContext context = HttpContext.Current;
            context.Response.StatusCode = 401;
            context.Response.AddHeader(
                "WWW-Authenticate",
                String.Format("Basic realm=\"{0}\"", config.AuthName));
        }
        #endregion

        #region .htaccess loader
        private bool LoadHtAccess()
        {
            HttpContext context = HttpContext.Current;

            // try to find .htaccess
            // recursively up to web site root folder
            string dir = context.Request.PhysicalPath;
            string rootDir = context.Request.PhysicalApplicationPath;

            return LoadHtAccessRecursively(dir, rootDir);
        }

        private bool LoadHtAccessRecursively(string dir, string rootDir)
        {
            string path = Path.Combine(dir, HTACCESS_FILENAME);
            string cacheKey = HTACCESS_FILENAME + path;

            // try getting config from cache
            config = (HtAccess)HttpContext.Current.Cache[cacheKey];
            if (config != null)
                return true;

            if (File.Exists(path))
            {
                // parse .htaccess
                config = new HtAccess();
                config.Path = path;
                config.RequiredUsers = new List<string>();
                config.RequiredGroups = new List<string>();

                string[] lines = File.ReadAllLines(path);
                foreach (string line in lines)
                {
                    if (line.Trim() == "")
                        continue; // skip empty lines
                    else if (line.StartsWith("#"))
                        continue; // skip comments
                    else if (line.StartsWith("AuthName "))
                        config.AuthName = ParseAuthName(line);
                    else if (line.StartsWith("AuthType "))
                        config.AuthType = ParseAuthType(line);
                    else if (line.StartsWith("AuthUserFile "))
                        config.Users = ParseAuthUserFile(line);
                    else if (line.StartsWith("AuthGroupFile "))
                        config.Groups = ParseAuthGroupFile(line);
                    else if (line.StartsWith("Require "))
                        ParseRequire(line);
                }

                // validate .htaccess
                if (string.IsNullOrEmpty(config.AuthName))
                    throw new HtAccessException(config.Path, "AuthName directive not found.");

                if (config.AuthType == AuthType.None)
                    config.AuthType = AuthType.Basic; // "Basic" authentication by default

                if(string.IsNullOrEmpty(config.AuthUserFile))
                    throw new HtAccessException(config.Path, "AuthUserFile directive not found.");

                // expand groups
                if(config.RequiredGroups.Count > 0 && config.Groups == null)
                    throw new HtAccessException(config.Path, "Require directive contains groups, however AuthGroupFile is not specified.");

                if(config.RequiredGroups.Count > 0 && config.Groups.Count == 0)
                    throw new HtAccessException(config.AuthGroupFile, "Require directive contains groups, however AuthGroupFile contains no groups.");

                foreach (string group in config.RequiredGroups)
                {
                    if (config.Groups.ContainsKey(group))
                        config.RequiredUsers.AddRange(config.Groups[group]);
                }

                // add to cache
                List<string> paths = new List<string>();
                paths.Add(path);
                if (!String.IsNullOrEmpty(config.AuthUserFile))
                    paths.Add(config.AuthUserFile);
                if (!String.IsNullOrEmpty(config.AuthGroupFile))
                    paths.Add(config.AuthGroupFile);
                HttpContext.Current.Cache.Insert(cacheKey, config, new CacheDependency(paths.ToArray()));

                return true;
            }
            else if (RemoveSlash(dir) == RemoveSlash(rootDir))
            {
                return false; // site root reached
            }
            else if (File.Exists(dir))
            {
                return LoadHtAccessRecursively(Path.GetDirectoryName(dir), rootDir);
            }
            else
                return LoadHtAccessRecursively(Directory.GetParent(dir).FullName, rootDir);
        }

        private string ParseAuthName(string line)
        {
            string authName = line.Substring(8).Trim();
            if (authName.StartsWith("\""))
                authName = authName.Substring(1, authName.Length - 2);
            return authName;
        }

        private AuthType ParseAuthType(string line)
        {
            string val = line.Substring(8).Trim();
            AuthType authType = AuthType.None;
            try
            {
                authType = (AuthType)Enum.Parse(typeof(AuthType), val, true);
            }
            catch
            {
                throw new HtAccessException(config.Path, line, "Unknown authentication type in AuthType directive");
            }

            if(authType == AuthType.Digest)
                throw new HtAccessException(config.Path, line, "Digest authentication is not supported. Change to \"Basic\" authentication.");

            return authType;
        }

        private Dictionary<string, string> ParseAuthUserFile(string line)
        {
            // calculate path
            string path = line.Substring(12).Trim().Replace("/", "\\");

            if (path.StartsWith("\\"))
            {
                // convert to absolute path
                path = Path.Combine(HttpContext.Current.Request.PhysicalApplicationPath, path.Substring(1));
            }

            if (!File.Exists(path))
                throw new HtAccessException(path, line, "AuthUserFile file is not found");

            config.AuthUserFile = path;

            Dictionary<string, string> users = new Dictionary<string, string>();
            string[] userLines = File.ReadAllLines(path);
            foreach (string userLine in userLines)
            {
                if (userLine.Trim() == "")
                    continue; // skip empty lines

                int colon = userLine.IndexOf(":");
                string username = userLine.Substring(0, colon).Trim();
                string password = userLine.Substring(colon + 1).Trim();
                if (!users.ContainsKey(username))
                    users.Add(username, password);
            }
            return users;
        }

        private Dictionary<string, List<string>> ParseAuthGroupFile(string line)
        {
            // calculate path
            string path = line.Substring(13).Trim().Replace("/", "\\");

            if (path.StartsWith("\\"))
            {
                // convert to absolute path
                path = Path.Combine(HttpContext.Current.Request.PhysicalApplicationPath, path.Substring(1));
            }

            if (!File.Exists(path))
                throw new HtAccessException(path, line, "AuthUserFile file is not found");

            config.AuthGroupFile = path;

            Dictionary<string, List<string>> groups = new Dictionary<string, List<string>>();
            string[] groupLines = File.ReadAllLines(path);
            foreach (string groupLine in groupLines)
            {
                if (groupLine.Trim() == "")
                    continue; // skip empty lines

                int colon = groupLine.IndexOf(":");
                string groupName = groupLine.Substring(0, colon).Trim();
                if (!groups.ContainsKey(groupName))
                {
                    // add group and its users
                    string[] groupUsers = groupLine.Substring(colon + 1).Trim().Split(' ', '\t');
                    List<string> users = new List<string>();
                    foreach (string user in groupUsers)
                    {
                        if (user.Trim() != "")
                            users.Add(user);
                    }
                    groups.Add(groupName, users);
                }
            }
            return groups;
        }

        private void ParseRequire(string line)
        {
            List<string> list = config.RequiredUsers;
            string[] accounts = line.Substring(7).Trim().Split(' ', '\t');
            int count = 0;
            foreach (string token in accounts)
            {
                string account = token.Trim();
                if (account == "")
                    continue; // skip empty tokens

                if (count == 0 && String.Compare("valid-user", account, true) == 0)
                    list.Add(account.ToLower());
                else if (count == 0 && String.Compare("group", account, true) == 0)
                    list = config.RequiredGroups;
                else if (count > 0 || String.Compare("user", account, true) != 0) // skip "user" directive
                    list.Add(account);

                count++;
            }

            if(count == 0)
                throw new HtAccessException(config.Path, line, "Require directive list is empty");
        }
        #endregion

        #region Misc utils
        private string RemoveSlash(string path)
        {
            return path.EndsWith("\\") ? path.Substring(0, path.Length - 1) : path;
        }

        private void WriteMessage(string message)
        {
            HttpContext context = HttpContext.Current;
            context.Response.Clear();
            context.Response.Write("<b>IIS Modules</b> - " + message);
            context.Response.End();
        }

        private void WriteError(string message, string path, string line)
        {
            HttpContext context = HttpContext.Current;
            context.Response.Clear();
            context.Response.Write("<html><head><title>Error</title></head><body>");
            context.Response.Write("<h1 style=\"color:#ff0000;\">WebsitePanel IIS Modules Error</h1>");
            context.Response.Write("<h2>" + message + "</h2>");
            if (path != null)
                context.Response.Write("<p><code>File: " + path + "</code></p>");
            if (line != null)
                context.Response.Write("<p><code>Line: " + line + "</code></p>");
            context.Response.Write("</body></html>");
            context.Response.End();
        }
        #endregion
    }
}
